package com.adee.java.nio;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
 * Buffer是NIO中用于存取数据的缓冲区
 *
 * java NIO提供的零拷贝方式
 * 1. MappedByteBuffer
 * MappedByteBuffer底层实现为linux提供的mmap方式。测试代码如下：
 *
 * 2.
 */
public class ByteBufferTest {

    public static void main(String[] args) throws Exception {
//        test1();
//        test2();
//        test3();
//        test4();
//        test5();
//        test5_1();
//        test6();
        test7();
    }


    public static void test1() {
        String str = "abcde";

        //分配一个指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        System.out.println("---------allocate-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //1024
        System.out.println(byteBuffer.position());   //0

        //利用 put() 存入数据到缓冲区中
        byteBuffer.put(str.getBytes());
        System.out.println("---------put-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //1024
        System.out.println(byteBuffer.position());   //5

        //切换到读数据模式
        byteBuffer.flip();
        System.out.println("---------flip-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //5,limit 表示可以操作数据的大小,只有 5 个字节的数据给你读,所以可操作数据大小是 5
        System.out.println(byteBuffer.position());   //0,读数据要从第 0 个位置开始读

        //利用 get() 读取缓冲区中的数据
        byte[] dst = new byte[byteBuffer.limit()];
        byteBuffer.get(dst);
        System.out.println(new String(dst, 0, dst.length));
        System.out.println("---------get-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //5,可以读取数据的大小依然是 5 个
        System.out.println(byteBuffer.position());   //5,读完之后位置变到了第 5 个

        //rewind() 可重复读
        byteBuffer.rewind();         //这个方法调用完后,又变成了读模式
        System.out.println("---------rewind-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //5
        System.out.println(byteBuffer.position());  //0

        // clear() 清空缓冲区,虽然缓冲区被清空了，但是缓冲区中的数据依然存在，只是出于"被遗忘"状态。意思其实是，缓冲区中的界限、位置
        // 等信息都被置为最初的状态了，所以你无法再根据这些信息找到原来的数据了，原来数据就处于"被遗忘"状态
        byteBuffer.clear();
        System.out.println("---------clear-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //1024
        System.out.println(byteBuffer.position());  //0
    }

    public static void test2() {
        String str = "abcde";
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put(str.getBytes());
        byteBuffer.flip();
        byte[] bytearray = new byte[byteBuffer.limit()];
        byteBuffer.get(bytearray, 0, 2);
        System.out.println(new String(bytearray, 0, 2));  //结果是 ab
        System.out.println(byteBuffer.position());   //结果是 2
        //标记一下当前 position 的位置
        byteBuffer.mark();
        byteBuffer.get(bytearray, 2, 2);
        System.out.println(new String(bytearray, 2, 2));
        System.out.println(byteBuffer.position());   //结果是 4
        //reset() 恢复到 mark 的位置
        byteBuffer.reset();
        System.out.println(byteBuffer.position());   //结果是 2

        //判断缓冲区中是否还有剩余数据
        if (byteBuffer.hasRemaining()) {
            //获取缓冲区中可以操作的数量
            System.out.println(byteBuffer.remaining());  //结果是 3,上面 position 是从 2 开始的
        }
    }

    public static void test3() {
        // 分配直接缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
        // 判断是直接缓冲区还是非直接缓冲区
        System.out.println(byteBuffer.isDirect());
    }

    public static void test4() {
        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
        byte[] bytes = "我这句话超过是个字节了".getBytes(StandardCharsets.UTF_8);
        byte[] dst = new byte[1024];
        for (int i = 0; i < bytes.length/3+(bytes.length%3==0?0:1); i++) {
            byteBuffer.clear();
            byteBuffer.put(bytes, i*byteBuffer.capacity(), byteBuffer.capacity());
            if (byteBuffer.position() > 0) {
                byteBuffer.flip();
                byteBuffer.get(dst, i*byteBuffer.capacity(), byteBuffer.capacity());
            }
        }
        System.out.println("读完了");
    }

    // 两个文件之间拷贝数据，使用MappedByteBuffer提供的零拷贝，实现方式为linux的mmap
    public static void test5() throws Exception {
        String from = "F:\\mycode\\java\\jdk\\src\\main\\resources/txt/mmap.from.txt";
        FileChannel readChannel = FileChannel.open(Paths.get(from),
                StandardOpenOption.READ, StandardOpenOption.WRITE);
        MappedByteBuffer map = readChannel.map(FileChannel.MapMode.READ_WRITE, 0, readChannel.size());
        FileChannel writeChannel = FileChannel.open(Paths.get(
                "F:\\mycode\\java\\jdk\\src\\main\\resources/txt/mmap.to.txt"),
                StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ);
        // 传输数据
//        map.flip();
        map.clear();
        map.put("kkkk".getBytes(StandardCharsets.UTF_8));
        map.force();
        writeChannel.write(map);
        writeChannel.close();
        readChannel.close();
        System.out.println("文件拷贝完成");
    }
    /**
     * 假设只使用固定大小的内存10B（实际中可能是10M），向一个大文件（比如2G）写数据，就需要循环创建映射内存，
     * 映射到的文件地址每次后移10B（实际10M），注意每次创建新的mmap时，销毁上次的mmap以释放虚拟内存空间。
     * 只是一种想法，其实际效率未测试。
     */
    public static void test5_1() throws IOException {
        String txt = "F:\\mycode\\java\\jdk\\src\\main\\resources/txt/mmap.big.txt";
        File file = new File(txt);
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        // FileChannel fileChannel = FileChannel.open(Paths.get(txt),StandardOpenOption.READ, StandardOpenOption.WRITE);
        FileChannel fileChannel = raf.getChannel();
        long fileSize = fileChannel.size();
        long expectLen = 60;
        long actualLen = expectLen;
        long padRemain = fileSize%expectLen;
        long cnt = padRemain == 0 ? fileSize/expectLen : (fileSize/expectLen+1);
        MappedByteBuffer map = null;
        for (int i = 0; i < cnt; i++) {
            try {
                if(i == cnt-1) {
                    if(padRemain > 0)
                    actualLen = padRemain;
                }
                // 创建mmap
                map = fileChannel.map(FileChannel.MapMode.READ_WRITE, i*expectLen, actualLen);
                // 写mmap
                map.clear();
                // 实际字节数超过映射空间长度会抛异常BufferOverflowException
                map.put("fff".getBytes(StandardCharsets.UTF_8));
                // 不用强制刷新，munmap时自动刷新
                //map.force();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if(map != null) {
                    // munmap
                    try {
                        munmap(fileChannel, map);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

        }
        //fileChannel.close();
        //boolean d = file.delete(); // false，mmap存在时，删除失败，需要munmap
        //System.out.println("deleted: " + d);
        System.out.println("mmap.big.txt写完成");
    }

    /**
     * FileChannelImpl.unmap是private的，需要反射调用
     */
    public static void munmap(FileChannel fileChannel, MappedByteBuffer buffer) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 加上这几行代码,手动unmap
        Method m = fileChannel.getClass().getDeclaredMethod("unmap",
                MappedByteBuffer.class);
        m.setAccessible(true);
        m.invoke(fileChannel.getClass(), buffer);
        m.setAccessible(false);
    }

    /**
     * 使用FileChannel的transferTo()/transferFrom()在两个fd之间拷贝数据，底层实现就是sendfile()系统调用函数。
     * 真正的零拷贝，Kafka这个开源项目就是用的这种方式。
     */
    public static void test6() throws IOException {
        String from = "F:\\mycode\\java\\jdk\\src\\main\\resources/txt/sendfile.from.txt";
        FileChannel readChannel = FileChannel.open(Paths.get(from), StandardOpenOption.READ);
        String to = "F:\\mycode\\java\\jdk\\src\\main\\resources/txt/sendfile.to.txt";
        FileChannel writeChannel = FileChannel.open(Paths.get(to),
                StandardOpenOption.CREATE, StandardOpenOption.WRITE);
        // 传输数据
        readChannel.transferTo(0, readChannel.size(), writeChannel);
        readChannel.close();
        writeChannel.close();
        System.out.println("文件拷贝完成");
    }

    /**
     * 直接内存和堆内存两种方式创建ByteBuffer速度比较
     * 结论：直接内存的创建和释放要比堆内存耗费性能，因此直接内存适合较大且驻留时间较长的场景，不宜频繁创建和销毁。
     * netty中使用直接内存就是先申请一块比较大的直接内存，然后自己实现内存分配算法（伙伴算法）来管理内存的分配和释放。
     */
    public static void test7() throws IOException {
        int cnt = 10000;
        int size = 3000;
        long t1 = System.currentTimeMillis();
        for (int i = 0; i < cnt; i++) {
            ByteBuffer.allocate(size);
        }
        long t2 = System.currentTimeMillis();
        for (int i = 0; i < cnt; i++) {
            ByteBuffer.allocateDirect(size);
        }
        long t3 = System.currentTimeMillis();

        System.out.println("HeapByteBuffer:" + (t2-t1)); // 26
        System.out.println("DirectByteBuffer:" + (t3-t1)); // 85
    }
}
